﻿using System;
using System.Configuration;
using System.Data.SqlClient;
using System.IO.Compression;
using System.Runtime.Serialization.Formatters.Binary;
using System.Security.Cryptography;
using System.Text;
using FluentAssertions;
using KvDbApi;
using Microsoft.VisualStudio.TestTools.UnitTesting;

namespace KvDb.UsageTests.BinaryScenario
{
    [TestClass]
    public class BinaryUsage
    {
        private static readonly string connectionString = ConfigurationManager.ConnectionStrings["SqlKvDb"].ConnectionString;

        [TestMethod, TestCategory("SQL Server Integrated Test")]
        public void SaveGetAndUpdate()
        {
            // Example only, keys are typically known unique business values:
            var key = "SaveGetAndUpdate." + Guid.NewGuid() + "#binaryUsage";
            
            var instance1 = new BinaryDataObject();
            instance1.Value1 = "testme";
            instance1.Value2 = 5;
            instance1.Children.Add(new BinaryChildObject { SubValue = "ChildValue" });

            // Initial save, don't provide a version number:
            var valueHandle1 = new ValueHandle(key);
            using (var c = new SqlConnection(connectionString))
            using (var s = valueHandle1.GetWriterStream(c))
            {
                var formatter = new BinaryFormatter();
                formatter.Serialize(s, instance1);
            }

            // Initial get, don't provide a version number.
            var valueHandle2 = new ValueHandle(key);

            BinaryDataObject instance2;
            using (var c = new SqlConnection(connectionString))
            using (var r = valueHandle2.GetReaderStream(c))
            {
                var formatter = new BinaryFormatter();
                instance2 = formatter.Deserialize(r) as BinaryDataObject;
            }

            // Compare for reference inequality and value equality:
            instance2.Should().NotBeSameAs(instance1);
            instance2.Value1.Should().Be(instance1.Value1);
            instance2.Value2.Should().Be(instance1.Value2);
            instance2.Children.Count.Should().Be(1);
            instance2.Children[0].Should().NotBeSameAs(instance1.Children[0]);
            instance2.Children[0].SubValue.Should().Be(instance1.Children[0].SubValue);

            // make an update, using the value handle from the get scenario to maintain version concurrency:
            instance2.Value2 = 10;
            instance2.Children[0].SubValue = "Item1";
            instance2.Children.Add(new BinaryChildObject { SubValue = "Item2" });
            using (var c = new SqlConnection(connectionString))
            using (var s = valueHandle2.GetWriterStream(c))
            {
                var formatter = new BinaryFormatter();
                formatter.Serialize(s, instance2);
            }

            // Get again and verify the updates:
            BinaryDataObject instance3;
            using (var c = new SqlConnection(connectionString))
            using (var r = valueHandle2.GetReaderStream(c))
            {
                var formatter = new BinaryFormatter();
                instance3 = formatter.Deserialize(r) as BinaryDataObject;
            }

            instance3.Value2.Should().Be(10);
            instance3.Children.Count.Should().Be(2);
            instance3.Children[0].SubValue.Should().Be("Item1");
            instance3.Children[1].SubValue.Should().Be("Item2");
        }

        [TestMethod, TestCategory("SQL Server Integrated Test")]
        public void SaveGetAndUpdateCompressed()
        {
            var instance1 = new BinaryDataObject();
            instance1.Value1 = "testme";
            instance1.Value2 = 5;
            instance1.Children.Add(new BinaryChildObject { SubValue = "ChildValue" });

            // Example only, keys are typically known unique business values:
            var key = "SaveGetAndUpdate." + Guid.NewGuid() + "#binaryUsage";

            // Initial save, don't provide a version number:
            var valueHandle1 = new ValueHandle(key);
            using (var c = new SqlConnection(connectionString))
            using (var s = valueHandle1.GetWriterStream(c))
            using (var gzip = new GZipStream(s, CompressionLevel.Fastest))
            {
                var formatter = new BinaryFormatter();
                formatter.Serialize(gzip, instance1);
            }

            // Initial get, don't provide a version number.
            var valueHandle2 = new ValueHandle(key);

            BinaryDataObject instance2;
            using (var c = new SqlConnection(connectionString))
            using (var r = valueHandle2.GetReaderStream(c))
            using (var gzip = new GZipStream(r, CompressionMode.Decompress))
            {
                var formatter = new BinaryFormatter();
                instance2 = formatter.Deserialize(gzip) as BinaryDataObject;
            }

            // Compare for reference inequality and value equality:
            instance2.Should().NotBeSameAs(instance1);
            instance2.Value1.Should().Be(instance1.Value1);
            instance2.Value2.Should().Be(instance1.Value2);
            instance2.Children.Count.Should().Be(1);
            instance2.Children[0].Should().NotBeSameAs(instance1.Children[0]);
            instance2.Children[0].SubValue.Should().Be(instance1.Children[0].SubValue);

            // make an update, using the value handle from the get scenario to maintain version concurrency:
            instance2.Value2 = 10;
            instance2.Children[0].SubValue = "Item1";
            instance2.Children.Add(new BinaryChildObject { SubValue = "Item2" });
            using (var c = new SqlConnection(connectionString))
            using (var s = valueHandle2.GetWriterStream(c))
            using (var gzip = new GZipStream(s, CompressionLevel.Fastest))
            {
                var formatter = new BinaryFormatter();
                formatter.Serialize(gzip, instance2);
            }

            // Get again and verify the updates:
            BinaryDataObject instance3;
            using (var c = new SqlConnection(connectionString))
            using (var r = valueHandle2.GetReaderStream(c))
            using (var gzip = new GZipStream(r, CompressionMode.Decompress))
            {
                var formatter = new BinaryFormatter();
                instance3 = formatter.Deserialize(gzip) as BinaryDataObject;
            }

            instance3.Value2.Should().Be(10);
            instance3.Children.Count.Should().Be(2);
            instance3.Children[0].SubValue.Should().Be("Item1");
            instance3.Children[1].SubValue.Should().Be("Item2");
        }

        [TestMethod, TestCategory("SQL Server Integrated Test")]
        public void SaveGetAndUpdateEncrypted()
        {
            var instance1 = new BinaryDataObject();
            instance1.Value1 = "testme";
            instance1.Value2 = 5;
            instance1.Children.Add(new BinaryChildObject { SubValue = "ChildValue" });

            // Example only, keys are typically known unique business values:
            var key = "SaveGetAndUpdate." + Guid.NewGuid() + "#binaryUsage";
            
            // Set up crypto provider. The init vector is 
            // a constant (normmally protected) secret, and 
            // this example uses the storage key as the 
            // encryption key:
            var cryptoProvider = new RijndaelManaged();
            cryptoProvider.IV = new byte[16] {0,1,2,3,4,5,6,7,8,9,10,11,12,13,14,15};
            cryptoProvider.Key = SHA256Cng.Create().ComputeHash(Encoding.UTF8.GetBytes(key));

            var encryptor = cryptoProvider.CreateEncryptor(cryptoProvider.Key, cryptoProvider.IV);
            var decryptor = cryptoProvider.CreateDecryptor(cryptoProvider.Key, cryptoProvider.IV);

            // Initial save, don't provide a version number:
            var valueHandle1 = new ValueHandle(key);
            using (var c = new SqlConnection(connectionString))
            using (var s = valueHandle1.GetWriterStream(c))
            using (var cryptoStream = new CryptoStream(s, encryptor, CryptoStreamMode.Write))
            {
                var formatter = new BinaryFormatter();
                formatter.Serialize(cryptoStream, instance1);
            }

            // Initial get, don't provide a version number.
            var valueHandle2 = new ValueHandle(key);

            BinaryDataObject instance2;
            using (var c = new SqlConnection(connectionString))
            using (var r = valueHandle2.GetReaderStream(c))
            using (var cryptoStream = new CryptoStream(r, decryptor, CryptoStreamMode.Read))
            {
                var formatter = new BinaryFormatter();
                instance2 = formatter.Deserialize(cryptoStream) as BinaryDataObject;
            }

            // Compare for reference inequality and value equality:
            instance2.Should().NotBeSameAs(instance1);
            instance2.Value1.Should().Be(instance1.Value1);
            instance2.Value2.Should().Be(instance1.Value2);
            instance2.Children.Count.Should().Be(1);
            instance2.Children[0].Should().NotBeSameAs(instance1.Children[0]);
            instance2.Children[0].SubValue.Should().Be(instance1.Children[0].SubValue);

            // make an update, using the value handle from the get scenario to maintain version concurrency:
            instance2.Value2 = 10;
            instance2.Children[0].SubValue = "Item1";
            instance2.Children.Add(new BinaryChildObject { SubValue = "Item2" });
            using (var c = new SqlConnection(connectionString))
            using (var s = valueHandle2.GetWriterStream(c))
            using (var cryptoStream = new CryptoStream(s, encryptor, CryptoStreamMode.Write))
            {
                var formatter = new BinaryFormatter();
                formatter.Serialize(cryptoStream, instance2);
            }

            // Get again and verify the updates:
            BinaryDataObject instance3;
            using (var c = new SqlConnection(connectionString))
            using (var r = valueHandle2.GetReaderStream(c))
            using (var cryptoStream = new CryptoStream(r, decryptor, CryptoStreamMode.Read))
            {
                var formatter = new BinaryFormatter();
                instance3 = formatter.Deserialize(cryptoStream) as BinaryDataObject;
            }

            instance3.Value2.Should().Be(10);
            instance3.Children.Count.Should().Be(2);
            instance3.Children[0].SubValue.Should().Be("Item1");
            instance3.Children[1].SubValue.Should().Be("Item2");
        }
    }
}
